# 根据xml文件创建UI类
import sys
import os
import time
import math
import re,json
import traceback
from collections import defaultdict

from PyQt5.QtWidgets import (QApplication, QButtonGroup, QRadioButton, QCheckBox, QComboBox,QTabWidget,QMessageBox,QTextEdit,QFrame,QAction,
                             QWidget, QLineEdit, QLabel, QPushButton, QFileDialog, QHBoxLayout, QGridLayout, QVBoxLayout, QListView)
from PyQt5.QtCore import Qt, QTimer, QBasicTimer, QDate, QAbstractListModel, QStringListModel, QEvent,pyqtSignal,QObject
from PyQt5.QtGui import QPixmap,QColor,QIcon,QMouseEvent

from base.CommonWin import BaseMainWin, BaseTabWin
from XmlReaderExpat import XmlParserSax, XmlElement
from enum import Enum
import codecs

ROOT_PATH = os.path.split(__file__)[0]
print("ROOT_PATH",ROOT_PATH)
ASSETS_PATH = os.path.join(ROOT_PATH,"assets")
CONFIG_PATH = os.path.join(ASSETS_PATH,"config")
TEMP_PATH = os.path.join(ASSETS_PATH,"template")
VERSION_PATH = os.path.join(ASSETS_PATH,"file_version_info.txt")
regFnName = re.compile(r"#[a-zA-Z0-9_]+#")
regName = re.compile(r"^n\d+$")
regModuleName = re.compile(r"^[a-zA-Z]+$") 
regClsName = re.compile(r"^[a-zA-Z_]+[a-zA-Z_0-9]*$") 
xmlParse = XmlParserSax()
dataTypeConfig = None
toolConfig = None
versionInf = None
fnTempMap = defaultdict(str)

versionCN = {
    "CompanyName":"公司名",
    "FileVersion":"文件版本",
    "LegalCopyright":"版权",
    "OriginalFilename":"原始文件名",
    "ProductName":"产品名",
    "ProductVersion":"产品版本"
    
}

class EDisplayType(Enum):
    image = 0
    Button = 1
    text = 2
    richtext = 3
    loader = 4
    component = 5
    controller = 6
    list = 7
    baseitem = 8
    group = 9

class EToolCfg(Enum):
    modulePath = 1 #模块目录
    author = 2 #作者
    autoBtn = 3 #自动按钮点击事件
    autoList = 4 #自动list点击事件
    fieldPro = 5 #字段限定符
    autoCtrl = 6 #自动为controller添加点击事件
    moduleName = 7 #记录上次使用的模块名
    fieldStyle = 8  #是否驼峰变量名
    autoMkProxy = 9 #自动创建Proxy代理类
    autoMkCtrl = 10 #自动创建controller类
    autoMkCache = 11 #自动创建cache类

class EFnTemp(Enum):
    """
    功能代码模板文件名枚举:
        点击函数模板
        proxy和controller类模板    
    Arguments:
        object {[type]} -- [description]
    """
    btnFn = 1
    listFn = 2
    ctrlFn = 3
    controller = 4
    proxy = 5
    cache = 6


def getToolCfgUrl():
    jsonUrl = os.path.join(CONFIG_PATH,"tool.conf")
    return jsonUrl

def initToolCfg():
    """
    初始化工具配置 tool.conf
    """
    global toolConfig
    jsonUrl = getToolCfgUrl()
    with open(jsonUrl,"r",encoding="utf8") as f:
        toolConfig = json.load(f)

def initVersionInf(versionfile):
    global versionInf
    vsArr = None
    with codecs.open(versionfile, 'rU', 'utf-8') as fp:        
        txt = fp.read()
        reg = re.compile(r"StringTable\([\s\S]+?\]\)\,") # [\s\S] 可以匹配任意字符 . 只能匹配非换行的任意字符
        mobj = reg.search(txt)
        verStr = mobj.group()
        verStr = verStr[:len(verStr)-4]
        verStr = verStr.replace("StringTable","")
        verStr = verStr.replace("StringStruct","")        
        verStr = re.sub("\s","",verStr) #去掉换行符 空白        
        vs = eval(verStr)
        vsArr = vs[1]            
    versionInf = vsArr

def getVersionInfStr():
    vs = versionInf
    n = len(vs)
    verStr = ""
    for i in range(n):
        s = vs[i]
        cn = versionCN.get(s[0],None)
        if cn:            
            verStr += cn + ":"+s[1]+"<br>"
    return verStr

def initDataTypeCfg():
    """
    初始化数据类型的配置 dataType.json
    """
    global dataTypeConfig
    jsonUrl = os.path.join(CONFIG_PATH,"dataType.json")
    with open(jsonUrl,"r",encoding="utf8") as f:
        dataTypeConfig = json.load(f)

def getToolCfg(key):
    return toolConfig.get(key,"")

def setToolCfg(key,value,isSave=True):
    toolConfig[key] = value
    if isSave:        
        jsonUrl = getToolCfgUrl()
        with open(jsonUrl,"w",encoding="utf8") as f:
            json.dump(toolConfig,f,indent=2,ensure_ascii=False) # ensure_ascii 避免英文乱码 indent格式化输出

def getDataTypeStr(name):
    dts = dataTypeConfig['dt'].get(name,"any")    
    return dts

def getDataVarAs(name):
    vas = dataTypeConfig['asVar'].get(name,"")
    return vas

def writeFile(fileName,content):    
    with open(fileName,"w",encoding="utf8") as f:
        f.write(content)


def getTemp(tempFile):
    """
        获取基类模板
    Arguments:
        tempFile {[str]} -- [无后缀的模板文件名]
    """
    tempFile = tempFile+".temp"
    retStr = ""
    path = os.path.join(TEMP_PATH,tempFile)
    if os.path.exists(path):
        with open(path,"r",encoding="utf8") as f:
            retStr = f.read()
    return retStr

def getFnTemp(tempFile):
    """
    获取功能模板文件内容
    
    Arguments:
        tempFile {[str]} -- [文件名 无后缀]
    """
    retStr = fnTempMap.get(tempFile)
    if not retStr:        
        tempFile = tempFile+".temp"    
        path = os.path.join(ASSETS_PATH,"functemp",tempFile)
        if os.path.exists(path):
            with open(path,"r",encoding="utf8") as f:
                retStr = f.read()
                fnTempMap[tempFile] = retStr             
    return retStr

def getFnTempName(tempFile):
    """获取函数目标里面的函数名称
    
    Arguments:
        tempFile {[str]} -- [不带后缀的目标文件名]
    """
    retStr = getFnTemp(tempFile)
    mobj = regFnName.search(retStr)
    fnStr = "onTestFn"
    if mobj:
        fnStr = mobj.group()
        fnStr = fnStr.replace("#","")
    return fnStr

def getTempList():
    """获取所有基类列表
    """
    tempList = [ x.split(".")[0] for x in os.listdir(TEMP_PATH) ]
    tempList.sort()    
    return tempList

def isCreateXmlNode(xmlEle):
    '''
    是否是需要生成代码的组件
    '''
    #print("isCreateXmlNode xmlEle.name",xmlEle.name)
    xmlNode = None
    mobj = regName.match(xmlEle.attr['name'])
    if not mobj:        
        if xmlEle.name==EDisplayType.component.name:        
            if isButton(xmlEle):                                    
                xmlNode = xmlEle
                xmlNode.name = EDisplayType.Button.name
            elif isBaseItem(xmlEle):
                xmlNode = xmlEle
                xmlNode.name = EDisplayType.baseitem.name

        elif xmlEle.name==EDisplayType.text.name or xmlEle.name==EDisplayType.richtext.name:
            if xmlEle.attr and xmlEle.attr['name'].startswith("txt_"):
                xmlNode = xmlEle
        elif xmlEle.name==EDisplayType.controller.name:
            if xmlEle.attr.get("name","")!="button": # 过滤掉名字为 button 的 controller
                xmlNode = xmlEle             
        else:        
            xmlNode = xmlEle

    return xmlNode

def isButton(xmlEle):    
    btnName = xmlEle.attr.get("name","")    
    flag = False
    if str(btnName).startswith("btn_") and btnName!="btn_close": #忽略所有 btn_close         
        flag = True #len(xmlEle.children)==1 and xmlEle.children[0].name==EDisplayType.Button.name  
    return flag 
def isBaseItem(xmlEle):
    name = xmlEle.attr.get("name","")
    flag = name.lower().startswith("baseitem")
    return flag  

def xmlToTsInf(xmlOrFile):
    print("xmlOrFile",xmlOrFile)
    xmlParse.updateByXml(xmlOrFile, False)
    controllers = xmlParse.xmlData.get("controller",[])
    displayList = xmlParse.xmlData.get("displayList",[])
    
    displayMap = defaultdict(list)
    if controllers and len(controllers)>0:
        displayMap[EDisplayType.controller.name] = []
        for c in controllers:
            c = isCreateXmlNode(c)
            if c:
                displayMap[EDisplayType.controller.name].append(c)

    if len(displayList)>0:        
        displayList = displayList[0]
        num = len(displayList.children)
        for i in range(num):
            node = isCreateXmlNode(displayList.children[i])
            if node:
                if not displayMap[node.name]:
                    displayMap[node.name] = []
                displayMap[node.name].append(node)

    print("displayMap",displayMap)
    print("displayList",displayList)
    return displayMap


class TabXMLToTS(QWidget):
    def __init__(self):
        super().__init__()
        self.moduleList = []
        self.setAcceptDrops(True)  # 接受拖放
        self.isSelectSearch = False
        self.tsInfMap = None
        curRow = 1

        # 关于 QGridLayout  https://blog.csdn.net/liang19890820/article/details/51538275
        self.mainBox = QGridLayout()
        self.mainBox.setAlignment(Qt.AlignTop)
        #self.mainBox.setColumnMinimumWidth(0,650)       
        self.setLayout(self.mainBox)

        self.padingLeft = 30
        self.padingTop = 50

        localConatainer = QWidget()
        hBox = QHBoxLayout()
        localConatainer.resize(300, 35)
        hBox.setAlignment(Qt.AlignTop)
        localConatainer.setLayout(hBox)
        self.mainBox.addWidget(localConatainer, curRow, 0)

        self.lblPath = QLabel()
        self.lblPath.setText("模块根目录：")
        #self.mainBox.addWidget(self.lblPath, curRow, 0)
        hBox.addWidget(self.lblPath,1)

       
        self.pathInput = QLineEdit() #模块根目录输入框
        self.pathInput.setPlaceholderText("模块目录")
        self.pathInput.textChanged.connect(self.onPathInputChange)
        py = self.pathInput.height()-self.lblPath.height()
        self.pathInput.move(
            self.lblPath.x()+self.lblPath.width()-30, self.lblPath.y()-5)
        #self.mainBox.addWidget(self.pathInput, curRow,1)
        hBox.addWidget(self.pathInput,20)
        

        self.btnBrowse = QPushButton()
        self.btnBrowse.setText("浏览")
        self.btnBrowse.setToolTip("浏览模块目录")        
        self.btnBrowse.clicked.connect(self.clickBtn)  # 注册点击事件
        #self.mainBox.addWidget(self.btnBrowse, curRow, 2)
        hBox.addWidget(self.btnBrowse,1)

        self.openBtn = QPushButton()
        self.openBtn.setText("打开")
        self.openBtn.setToolTip("打开项目的模块目录")        
        self.openBtn.clicked.connect(self.clickBtn)  # 注册点击事件
        #self.mainBox.addWidget(self.btnBrowse, curRow, 2)
        hBox.addWidget(self.openBtn,1)

        self.lblSearch = QLabel()
        self.lblSearch.setText("模块名:")           
        #self.mainBox.addWidget(self.lblSearch, curRow, 3)
        hBox.addWidget(self.lblSearch,1)

        self.searchInput = QLineEdit()
        self.searchInput.textChanged.connect(self.onSearchInputChange)
        self.searchInput.installEventFilter(self)
        self.searchInput.setPlaceholderText("保存TS的目录")
        self.searchInput.setToolTip(
            "不填则以模块根目录作为TS保存目录，否则以该目录作为保存路径（没有目录会自动创建）")
        #self.mainBox.addWidget(self.searchInput,curRow,4)
        hBox.addWidget(self.searchInput,10)

        self.checBox = QCheckBox()
        self.checBox.setText("忽略模块名保存TS")
        self.checBox.setToolTip("勾选后以模块根目录作为TS保存目录")
        #self.mainBox.addWidget(self.checBox, curRow, 5)
        hBox.addWidget(self.checBox,1)

        curRow += 1

        localConatainer = QWidget()
        hBox = QHBoxLayout()
        localConatainer.resize(300, 35)
        hBox.setAlignment(Qt.AlignTop)
        localConatainer.setLayout(hBox)
        self.mainBox.addWidget(localConatainer,curRow,0)

        self.model = QStringListModel(self)
        #self.model.setStringList(["a", "b", "c", "d"])

        lblName = QLabel()
        lblName.setText("类名:")
        #self.mainBox.addWidget(lblName, curRow, 0)
        hBox.addWidget(lblName,1)

        self.lblClsName = QLineEdit()
        self.lblClsName.setPlaceholderText("输入要生成的类名")
        #self.mainBox.addWidget(self.lblClsName, curRow, 1)
        hBox.addWidget(self.lblClsName,4)
        #hBox.setStretchFactor(self.lblClsName,12)

        lblName = QLabel()
        lblName.setText("类注释:")
        #self.mainBox.addWidget(lblName, curRow, 0)
        hBox.addWidget(lblName,1)

        self.annotationCls = QLineEdit()
        self.annotationCls.setPlaceholderText("输入类注释")
        #self.mainBox.addWidget(self.lblClsName, curRow, 1)
        hBox.addWidget(self.annotationCls,3)
        #hBox.setStretchFactor(self.annotationCls,8)

        lblName = QLabel()
        lblName.setText("选择父类:")        
        #self.mainBox.addWidget(lblName, curRow, 2)
        hBox.addWidget(lblName,1)

        self.ListRenderer = "ListRenderer"
        self.ModuleClsName = "BaseModule"
        self.baseClsList = getTempList()
        
        self.comBox = QComboBox()
        self.comBox.setGeometry(200, 50, 150, 30)
        self.comBox.activated[str].connect(self.onSelectBaseCls)
        self.comBox.addItems(self.baseClsList)
        self.curBaseCls = self.baseClsList[0]

        self.setClsText()
        #self.mainBox.addWidget(self.comBox, curRow, 3)
        hBox.addWidget(self.comBox,2)

        lblName = QLabel(localConatainer)
        lblName.setText("字段属性:")
        hBox.addWidget(lblName, 1)

        privateBtn = QRadioButton()
        privateBtn.setText("private")
        hBox.addWidget(privateBtn, 1)

        protectedBtn = QRadioButton()
        protectedBtn.setText("protected")
        #protectedBtn.setChecked(True)
        hBox.addWidget(protectedBtn, 1)
        
        publicBtn = QRadioButton()
        publicBtn.setText("public")
        hBox.addWidget(publicBtn,1)

        # 字段限定名 按钮组
        self.varProGroup = QButtonGroup()
        self.varProGroup.addButton(privateBtn)
        self.varProGroup.addButton(protectedBtn)
        self.varProGroup.addButton(publicBtn)
        self.varProGroup.buttonClicked.connect(self.radioBtnClick)
        
        curRow += 1
        
        localConatainer = QWidget()
        hBox = QHBoxLayout()
        localConatainer.resize(300, 35)
        hBox.setAlignment(Qt.AlignTop)
        localConatainer.setLayout(hBox)
        self.mainBox.addWidget(localConatainer, curRow,0)

        self.startBtn = QPushButton()
        self.startBtn.setText("开　始")        
        self.startBtn.clicked.connect(self.onClickStart)
        #self.mainBox.addWidget(self.startBtn, curRow, 0)
        hBox.addWidget(self.startBtn,1)

        self.btnClickEvtCb = QCheckBox()
        self.btnClickEvtCb.setText("自动为按钮添加点击事件")
        self.btnClickEvtCb.setToolTip("为所有按钮添加点击事件")
        #self.mainBox.addWidget(self.btnClickEvtCb, curRow, 1)
        hBox.addWidget(self.btnClickEvtCb,1)

        self.listEvtCb = QCheckBox()
        self.listEvtCb.setText("自动为List添加选择事件")
        self.listEvtCb.setToolTip("为所有List添加点击事件")
        #self.mainBox.addWidget(self.listEvtCb, curRow, 2)
        hBox.addWidget(self.listEvtCb,1)
        
        self.ctrlEvtCb = QCheckBox()
        self.ctrlEvtCb.setText("自动为controller添加选择事件")
        self.ctrlEvtCb.setToolTip("为所有controller添加点击事件")
        #self.mainBox.addWidget(self.listEvtCb, curRow, 2)
        hBox.addWidget(self.ctrlEvtCb,3)

        curRow += 1

        self.logText = QTextEdit()        
        #self.logText.setFocusPolicy(Qt.NoFocus)
        #self.logText.setReadOnly(True)
        self.logText.setAcceptDrops(False)
        self.logText.setFontWeight(30)
        self.logText.viewport().installEventFilter(self)
        #self.logText.cursorPositionChanged.connect(self.onLogPo)
        #self.logText.clicked.connect(self.logClick)
        #self.logText.setAttribute(Qt.WA_NoMousePropagation,False)
        self.logText.setPlaceholderText("日志输出")
        self.mainBox.addWidget(self.logText,curRow,0,1,1)

        self.searchResult = QListView(self)
        self.searchResult.resize(200, 200)
        self.searchResult.setModel(self.model)       
        self.searchResult.clicked.connect(self.selectSearch)
        self.searchResult.setVisible(False)

        self.padding = "    "        
        self.curXmlUrl = False
        self.updateSetting()
        #self.setClsText()

    def onLogPo(self,*arg):
        print("onLogPo",arg)

    def updateSetting(self):         
        fileName = getToolCfg(EToolCfg.modulePath.name)
        self.pathInput.setText(fileName)
        self.initModuleList(fileName)

        index = getToolCfg(EToolCfg.fieldPro.name)
        btns = self.varProGroup.buttons()
        index = min(index,len(btns)-1)
        self.proBtn = protectedBtn = btns[index]
        self.proBtn.setChecked(True)

        mName = getToolCfg(EToolCfg.moduleName.name)
        self.searchInput.setText(mName)

        flag = bool(getToolCfg(EToolCfg.autoBtn.name))
        self.btnClickEvtCb.setChecked(flag)

        flag = bool(getToolCfg(EToolCfg.autoList.name))
        self.listEvtCb.setChecked(flag)

        flag = bool(getToolCfg(EToolCfg.autoCtrl.name))
        self.ctrlEvtCb.setChecked(flag)
        
    def logClick(self,e):
        print("-----logClick",e)

    def isBaseList(self):
        """当前基类是否是 ListRenderer 
        """
        return self.curBaseCls==self.ListRenderer

    def isBaseModule(self):
        """当前基类是否是 BaseModule 
        """
        return self.curBaseCls==self.ModuleClsName

    def onSelectBaseCls(self,text):
        #print("onSelectBaseCls",text)
        self.curBaseCls = text
        self.setClsText()
        self.curTemp = getTemp(self.curBaseCls) #当前基类的模板代码

    def setClsText(self):
        oldStr = str(self.lblClsName.text())
        fix = "UI_"
        if oldStr.startswith(fix) or oldStr=="" or not self.isClsNameOk(oldStr):
            clsName = self.getBaseClsEnd(self.curBaseCls)        
            clsName = fix+self.curBaseCls if clsName==self.curBaseCls else clsName
            self.lblClsName.setText(clsName)

        if self.isBaseList() and self.curXmlUrl and os.path.isfile(self.curXmlUrl): #当前基类是ListRender 类名直接是 xml文件名
            content = os.path.split(self.curXmlUrl)[1].split(".")[0]
            self.lblClsName.setText(content)
      
    def getBaseClsEnd(self,clsName):
        """根据基类名称获取基类的后缀名称
        例如： BaseTabPanel 会得到 Panel
        
        Arguments:
            clsName {[str]} -- [基类名称]
        """
        n = len(clsName)
        if self.isLegalModule():            
            for i in range(n):
                idx = n-i-1
                char = clsName[idx]  #从字符串的后面开始找          
                if char.istitle(): #找到第一个大写字母
                    clsName = self.searchInput.text().title() + clsName[idx:]                
                    break
        return clsName

    def onClickStart(self,e):        
        if self.pathInput.text()!="" and os.path.isdir(self.pathInput.text()):
            checked = self.checBox.isChecked() 
            if checked or self.isLegalModule():                
                curClsName = self.lblClsName.text()
                if not self.isClsNameOk(curClsName):
                    QMessageBox.information(self,"提示","类名只能包含字母、下划线、数字")
                    return
                if self.tsInfMap:
                    if self.curTemp and curClsName!="":                    
                        self.curClsName = curClsName #当前生成的类名 也是文件名
                        try:
                            self.makeTsFile()
                        except Exception as e:
                            self.appendLog("创建文件发生异常了-_- :")
                            self.appendLog(traceback.format_exc())
                            
                else:
                    QMessageBox.information(self,"提示","请把GUI的XML文件拖进来")                    
                #self.tsInfMap = None
            else:
                QMessageBox.warning(self,"警告","模块名只能纯字母")
        else:            
            # 参数顺序：标题，msg，按钮排列顺序，默认焦点按钮
            replay = QMessageBox.question(self,"系统提示","请先设置模块路径",QMessageBox.Yes,QMessageBox.Yes)
        setToolCfg(EToolCfg.moduleName.name,self.searchInput.text())

    def isClsNameOk(self,clsName):
        mobj = regClsName.match(clsName)
        return mobj!=None 

    def radioBtnClick(self, btn):
        self.proBtn = btn

    def eventFilter(self, watch, event): #事件监听器;需要通过 installEventFilter 注册
        if watch == self.searchInput:
            if event.type() == QEvent.FocusOut:
                pass               
            elif event.type() == QEvent.FocusIn:
                #print("搜索输入框得到焦点",self.searchInput.x())
                self.showSearchResult(True)      
        elif watch == self.logText.viewport() and isinstance(event,QMouseEvent) and event.type()==QMouseEvent.MouseButtonRelease:                    
            self.searchResult.setVisible(False)
        return super().eventFilter(watch, event)

    def mousePressEvent(self,e):
        #print("mousePressEvent ", e, time.time())        
        self.searchResult.setVisible(False)

    def dragEnterEvent(self, e):  # 拖拽进入区域
        if e.mimeData().hasUrls():  
            e.accept()

    def dropEvent(self, e):  # 放开了        
        url = e.mimeData().text()
        fileType = os.path.splitext(url)[1]
        url = str(url).replace("file:///", "")
        self.curXmlUrl = url
        self.appendLog(self.curXmlUrl)
        if fileType == ".xml": #把xml转换成Python对象
            self.tsInfMap = xmlToTsInf(url)
            self.curTemp = getTemp(self.curBaseCls)
            
    def onSearchInputChange(self, *args):        
        if not self.isSelectSearch:
            self.showSearchResult(False)
        self.isSelectSearch = False

    def onPathInputChange(self):
        fileName = self.pathInput.text()
        self.initModuleList(fileName,True)

    def showSearchResult(self, isAll):
        text = self.searchInput.text()
        strList = []
        hasRet = True
        if not isAll:
            isAll = len(text)==0
        if isAll:
            strList = self.moduleList
        else:
            if len(text) > 0:
                for s in self.moduleList:
                    idx = str(s).lower().find(text.lower())
                    if idx == 0:
                        strList.append(s)        
        self.searchResult.setVisible(hasRet)
        if hasRet:               
            gPos = self.searchInput.parent().mapToGlobal(self.searchInput.pos())
            gPos = self.mapFromGlobal(gPos)                   
            self.searchResult.move(gPos.x(),gPos.y()+self.searchInput.height())
            #self.searchResult.move(self.searchInput.x(),self.searchInput.y()+self.searchInput.height())
        self.model.setStringList(strList)

    def selectSearch(self, modelIndex):  # PyQt5.QtCore.QModelIndex
        data = self.model.itemData(modelIndex)
        if data and data[0] and len(data[0]) > 0:
            self.isSelectSearch = True
            self.searchInput.setText(data[0])
            self.setFocus()
            self.searchResult.setVisible(False)

    def clickBtn(self, *arg):
        btn = self.sender()
        if btn==self.btnBrowse:
            self.browse()
        elif btn==self.openBtn and os.path.isdir(self.pathInput.text()):
            os.startfile(self.pathInput.text())

    def browse(self):
        #print("点击按钮了....", arg)
        #fileName,fileType = QFileDialog.getOpenFileName(self,"选中模块根目录")
        fileName = QFileDialog.getExistingDirectory(
            self, "选中模块根目录",getToolCfg(EToolCfg.modulePath.name)) #"E:/H5Game/Client/src/game/module"        

        self.setFocus()          
        self.initModuleList(fileName,True)

    def initModuleList(self, fileName,isSaveCfg=False):
        """初始化模块搜索列表
        
        Arguments:
            fileName {[str]} -- [模块根目录]
        
        Keyword Arguments:
            isSaveCfg {bool} -- [是否保存设置] (default: {False})
        """
        if os.path.exists(fileName) and os.path.isdir(fileName):
            if isSaveCfg: #是否保存配置
                setToolCfg(EToolCfg.modulePath.name,fileName)
            self.pathInput.setText(fileName)
            self.moduleList = [file for file in os.listdir(fileName) if not file.startswith(
                ".") and os.path.isdir(os.path.join(fileName, file))]
            if self.searchResult.isVisible():
                self.showSearchResult(False)
                
    def mkTempClassStr(self,tempClsStr,mkClassName,varStr,fnBodyStr,funcStr,clsDesc=""):
        """生成类模板代码
        
        Arguments:
            tempClsStr {str} -- [原始的类模板代码字符串]
            mkClassName {str} -- [要生成的类名]
            varStr {str} -- [字段定义字符串]
            fnBodyStr {str} -- [初始化UI的函数体字符串]
            funcStr {str} -- [所有监听函数字符串]
            clsDesc {str} -- [类注释]
        """
        tempClsStr = tempClsStr.replace(r"$cls$",mkClassName)
        m = self.getModuleName()
        tempClsStr = tempClsStr.replace(r"$module$",m) #生成模块名
        clsDesc = self.annotationCls.text() if clsDesc=="" else clsDesc
        tempClsStr = tempClsStr.replace(r"$desc$",clsDesc) #生成注释
        authorStr = getToolCfg(EToolCfg.author.name) #
        if not authorStr:
            authorStr = "[by H5Gui create]"                    
        tempClsStr = tempClsStr.replace(r"$author$",authorStr) #生成作者
        now = time.time()
        nowArr =  time.localtime(now) #把时间戳转换成时间数组
        nowStr = time.strftime("%Y-%m-%d %H:%M:%S")  #把时间数组格式化为时间字符串
        tempClsStr = tempClsStr.replace(r"$time$",nowStr) #生成时间
        content = os.path.split(self.curXmlUrl)[1].split(".")[0]
        tempClsStr = tempClsStr.replace(r"$contentName$",content) #生成UI组件名称
        tempClsStr = tempClsStr.replace(r"$var$",varStr)
        tempClsStr = tempClsStr.replace(r"$fnBody$",fnBodyStr)
        tempClsStr = tempClsStr.replace(r"$func$",funcStr)
        return tempClsStr

    def makeTsFile(self):
        """创建用户主动要求的TS类文件
        """
        st = time.time()        
        #要生成的显示对象类型；按照顺序生成在代码里
        orderType = [EDisplayType.controller,EDisplayType.group,EDisplayType.baseitem,EDisplayType.loader,EDisplayType.image,EDisplayType.text,EDisplayType.richtext,EDisplayType.Button,EDisplayType.list]
        count = len(orderType)        
        newLine = "\r"
        varStr = ""
        fnStr = self.padding*2+"//---- script make start ----"+newLine
        btnClickStr = ""
        listSelectStr = ""
        ctrlClickStr = ""
        for i in range(count):
            varArr = self.tsInfMap[orderType[i].name]
            if varArr and len(varArr)>0:
                for xml in varArr:                    
                    vs,fs = self.xmlToVarFnStr(xml)
                    varStr += vs+newLine
                    fnStr += fs+newLine
                    #生成 addEventListener 字符串
                    if xml.name == EDisplayType.Button.name and self.btnClickEvtCb.isChecked():
                        tempStr = self.padding*2+"this."+self.getFieldName(xml.attr['name'])+"."+"addClickListener(this.{0}, this);"+newLine              
                        btnClickStr+=tempStr.format(getFnTempName(EFnTemp.btnFn.name))
                    elif xml.name == EDisplayType.list.name and self.listEvtCb.isChecked():     
                        tempStr = self.padding*2+"this."+self.getFieldName(xml.attr['name'])+".list.addEventListener(fairygui.ItemEvent.CLICK,this.{0},this);"+newLine                
                        listSelectStr+=tempStr.format(getFnTempName(EFnTemp.listFn.name))
                    elif xml.name == EDisplayType.controller.name and self.ctrlEvtCb.isChecked():
                        tempStr = self.padding*2+"this."+self.getFieldName(xml.attr['name'])+".addEventListener(fairygui.StateChangeEvent.CHANGED, this.{0}, this);"+newLine
                        ctrlClickStr += tempStr.format(getFnTempName(EFnTemp.ctrlFn.name))
                    
        fnStr += newLine + ctrlClickStr + btnClickStr + listSelectStr
        fnStr += self.padding*2+"//---- script make end ----"+newLine        
        #按钮和list侦听函数字符串
        ctrlFuncStr = ""
        if ctrlClickStr:
            ctrlFuncStr = self.getClickEvtFnStr(EFnTemp.ctrlFn.name,EDisplayType.controller.name,"")+newLine

        btnFuncStr = ""
        if btnClickStr != "":            
            btnFuncStr = self.getClickEvtFnStr(EFnTemp.btnFn.name,EDisplayType.Button.name,"")+newLine

        listFuncStr = ""
        if listSelectStr!="":
            listFuncStr = self.getClickEvtFnStr(EFnTemp.listFn.name,EDisplayType.list.name,".list")+newLine
        
        funcStr = ctrlFuncStr + btnFuncStr + listFuncStr
        self.curMakeClsStr = self.mkTempClassStr(self.curTemp,self.curClsName,varStr,fnStr,funcStr)

        fileName = self.curClsName+".ts"
        moduleName = "" if self.checBox.isChecked() else self.searchInput.text()
        modulePath = self.pathInput.text() #模块根目录
        folder = os.path.join(modulePath,moduleName) #完整的某个模块目录
        isCreateModule = False
        if self.createModuleFolder(folder):    
            isCreateModule = True        
            self.initModuleList(modulePath)

        if (self.isBaseModule() or isCreateModule) and moduleName!="": #检测是否有proxy和controller 没有自动创建
            flag = bool(getToolCfg(EToolCfg.autoMkProxy.name))
            if flag:                
                self.createFnClass(folder,moduleName,"Proxy",EFnTemp.proxy.name,"模块代理类")
            flag = bool(getToolCfg(EToolCfg.autoMkCtrl.name))
            if flag:                
                self.createFnClass(folder,moduleName,"controller",EFnTemp.controller.name,"模块控制器")
            flag = bool(getToolCfg(EToolCfg.autoMkCache.name))
            if flag:                
                self.createFnClass(folder,moduleName,"Cache",EFnTemp.cache.name,"模块数据缓存")

        fileName = os.path.join(folder,fileName)
        flag = self.createClassFile(fileName,self.curClsName,self.curMakeClsStr)        
        if flag:
            ct = time.time() - st
            ctStr = "耗时%0.5f秒"%(ct)      
            self.appendLog("已经生成 "+fileName+" {0}".format(ctStr))

    def createFnClass(self,moduleFolder,moduleName,fixClsName,eFnTempName,clsDesc)->bool:
        """创建其他功能类(proxy或controller)
        
        Arguments:
            moduleFolder {[str]} -- [模块目录]
            moduleName {[str]} -- [模块名称]
            fixClsName {[str]} -- [类后缀]
            eFnTempName {[str]} -- [类模板名称]
            clsDesc {[str]} -- [类注释]
        
        Returns:
            bool -- [是否创建成功]
        """        
        clsModuleStr = self.curClsName if self.curClsName and self.curClsName.lower().find("module")!=-1 else moduleName.title()+"Module" #用index查找字符串 找不到会报错:substring not found
        fnClsName = self.getClsNameByModule(moduleName,fixClsName)
        fileName = os.path.join(moduleFolder,fnClsName+".ts")
        flag = False        
        if not os.path.exists(fileName):            
            fnTemp = getFnTemp(eFnTempName)
            fnClsStr = self.mkTempClassStr(fnTemp,fnClsName,"","","",clsDesc)
            fnClsStr = fnClsStr.replace("$clsModule$",clsModuleStr)
            flag = self.createClassFile(fileName,fnClsName,fnClsStr)
        if flag:
            self.appendLog("已为你创建{0}类在:{1}".format(fnClsName,fileName))
        return flag   

    def getClsNameByModule(self,moduleName,fixName):
        #moduleNameTitle = moduleName.title()        
        moduleName = moduleName[0].upper() + moduleName[1:len(moduleName)]
        return moduleName+fixName.title()

    def createClassFile(self,fileName,clsName,classStr)->bool:
        """创建类文件
        
        Arguments:
            fileName {[str]} -- [完整的文件路径]
            clsName {[str]} -- [类名]
            classStr {[str]} -- [类文件内容]
        
        Returns:
            bool -- [是否创建成功]
        """
        flag = True
        if os.path.exists(fileName):
            reply = QMessageBox.question(self,"提示","类{0}已经存在，是否覆盖?".format(clsName),QMessageBox.Yes | QMessageBox.No,QMessageBox.No)
            if reply==QMessageBox.Yes:            
                writeFile(fileName,classStr)     
            else:
                flag = False  
        else:
            writeFile(fileName,classStr)
        return flag

    def createModuleFolder(self,folder)->bool:
        """[创建模块文件夹]
        
        Arguments:
            folder {[str]} -- [模块完整路径]
        
        Returns:
            bool -- [是否创建成果]
        """
        isCreateModule = False
        if not os.path.exists(folder): #不存在的模块目录自动创建
            os.makedirs(folder)
            self.appendLog("已为你创建模块 {0} ".format(folder))            
            isCreateModule = True
        return isCreateModule

    def getClickEvtFnStr(self,tempName,displayName,proFix):
        FnTemp = getFnTemp(tempName)
        FnTemp = FnTemp.replace("#","")
        funcStr = self.fmtCaseStr(self.tsInfMap[displayName],proFix)
        funcStr = FnTemp.replace("$case$",funcStr)
        return funcStr

    def fmtCaseStr(self,xmlArr,proFix=""):
        caseStr = ""
        newLine = "\r"        
        for xmlEle in xmlArr:
            caseStr += self.padding*3 + "case this."+self.getFieldName(xmlEle.attr['name'])+proFix+":"+newLine + self.padding*4+"break;"+newLine
        return caseStr

    def getFieldName(self,fieldName):
        fieldArr = fieldName.split("_")
        flag = getToolCfg(EToolCfg.fieldStyle.name)
        if flag and len(fieldArr)>=2:
            arr = list(map(lambda x: x.title(),fieldArr))  
            arr[0] = arr[0].lower()
            fieldName = "".join(arr)             
        return fieldName
    
    def xmlToVarFnStr(self,xmlEle):
        """
        根据一个xml节点返回变量定义和初始化组件的代码字符串        
        Arguments:
            xmlEle {[XmlElement]} -- xml节点元素
        """

        varStr = ""
        fnStr = ""        
        proStr = self.proBtn.text()
        varStr = self.padding+proStr+" "+self.getFieldName(xmlEle.attr['name'])+":"+getDataTypeStr(xmlEle.name)+";"
        
        clsTrs = ""
        asVar = ""
        fnSrcStr = ""
        
        clsTrans = "$clsTrans$"
        asVarRegStr = "$asVar$"
        varName = "$name$"
        if xmlEle.name==EDisplayType.controller.name:
            fnSrcStr = 'this.getController("{0}")'
        elif self.isBaseList(): #listRender都是通过getChild获得子
            fnSrcStr = 'this.getChild("{0}")'        
        else:
            fnSrcStr = 'this.getGObject("{0}")'
        fnSrcStr = fnSrcStr.format(varName)
        fnSrcStr = clsTrans + fnSrcStr + asVarRegStr
        asVar = getDataVarAs(xmlEle.name)
        if xmlEle.name==EDisplayType.loader.name or isBaseItem(xmlEle):
            clsTrs = asVar
            asVar = ""
        if asVar!="":
            asVar = "."+asVar
        if clsTrs!="":
            clsTrs = "<{0}>".format(clsTrs)
        #print("clsTrs,xmlEle.name",clsTrs,xmlEle.name)
        fnSrcStr = fnSrcStr.replace(varName,xmlEle.attr['name'])
        fnSrcStr = fnSrcStr.replace(clsTrans,clsTrs)
        fnSrcStr = fnSrcStr.replace(asVarRegStr,asVar)

        if xmlEle.name==EDisplayType.list.name:
            fnSrcStr = 'new List({0})'.format(fnSrcStr)
        fnStr = self.padding*2+"this."+self.getFieldName(xmlEle.attr['name'])+" = "+fnSrcStr+";"
        return varStr,fnStr
    
    def getModuleName(self):
        m = "__your_defind__"
        mobj = regModuleName.match(self.searchInput.text())
        if self.isLegalModule():
            m = self.searchInput.text()
            m = str(m)[0].title()+m[1:]
        return m

    def isLegalModule(self):
        """
        是否有效的模块名
        """
        mobj = regModuleName.match(self.searchInput.text())
        return mobj!=None

    def  appendLog(self,msg):
        msg = "<font size='5' >{0}</font>".format(msg)
        self.logText.append(msg)

class CustomSignal(QObject): # 自定义信号槽
    closed = pyqtSignal([bool])

class SettingWin(QWidget):
    def __init__(self,parent:QWidget=None):
        super().__init__(parent,Qt.Window|Qt.MSWindowsFixedSizeDialogHint )
        self.c = CustomSignal()
        self.closed = self.c.closed 
       
        self.resize(400,300)
        self.setWindowModality(Qt.ApplicationModal) #模态化窗口 
        self.setWindowTitle("设置")
        lbl = QLabel(self)
        lbl.setText("类作者:")
        lbl.resize(50,30)
        lbl.setAlignment(Qt.AlignCenter)
        lbl.move(30,30)
        gap = 5
        self.authorTxt = QLineEdit(self)        
        self.authorTxt.move(lbl.x()+lbl.width(),lbl.y()+gap)
        self.authorTxt.textChanged.connect(self.onAuthorChange)
        
        lblName = QLabel(self)        
        lblName.setText("字段属性:")
        lblName.adjustSize() #自动大小,需要先设置text再重新渲染
        lblName.move(lbl.x(),self.authorTxt.y()+self.authorTxt.height()+gap)

        privateBtn = self.getRadioBtn("private",lblName.x()+lblName.width()+gap,lblName.y())        
        protectedBtn = self.getRadioBtn("protected",privateBtn.x()+privateBtn.width(),privateBtn.y())
        publicBtn = self.getRadioBtn("public",protectedBtn.x()+protectedBtn.width(),protectedBtn.y())

        # 字段限定名 按钮组
        self.varProGroup = QButtonGroup()
        self.varProGroup.addButton(privateBtn)
        self.varProGroup.addButton(protectedBtn)
        self.varProGroup.addButton(publicBtn)
        self.varProGroup.buttonClicked.connect(self.onRadioBtnClick)
        gap = 15
        self.autoBtnCB = self.getCheckBox("自动按钮事件",lbl.x(),publicBtn.y()+publicBtn.height()+gap)
        self.autoListCb = self.getCheckBox("自动List事件",self.autoBtnCB.x()+self.autoBtnCB.width()+gap,self.autoBtnCB.y())
        self.autoCtrlCb = self.getCheckBox("自动controller事件",self.autoListCb.x()+self.autoListCb.width()+gap,self.autoBtnCB.y())        
        self.fieldStyleCb = self.getCheckBox("驼峰变量名",lbl.x(),self.autoCtrlCb.y()+self.autoCtrlCb.height()+gap)
        autoTips = "在创建baseModule或者模块文件夹时自动检测创建"
        self.mkProxyCb = self.getCheckBox("自动创建proxy类",self.fieldStyleCb.x()+self.fieldStyleCb.width(),self.fieldStyleCb.y(),autoTips)
        self.mkCtrlCb = self.getCheckBox("自动创建controller类",self.mkProxyCb.x()+self.mkProxyCb.width(),self.mkProxyCb.y(),autoTips)
        self.mkCacheCb = self.getCheckBox("自动创建cache类",self.fieldStyleCb.x(),self.mkCtrlCb.y()+self.mkCtrlCb.height()+gap,autoTips)

        self.autoBtnCB.clicked.connect(self.onCbClick)
        self.autoListCb.clicked.connect(self.onCbClick)
        self.autoCtrlCb.clicked.connect(self.onCbClick)
        self.fieldStyleCb.clicked.connect(self.onCbClick)

        self.saveBtn = QPushButton(self)
        self.saveBtn.setText("保存")
        px = (self.width() - self.saveBtn.width())/2
        py = self.height()-self.saveBtn.height()-5        
        self.saveBtn.move(px,py)
        self.saveBtn.clicked.connect(self.onClickSave)
        self.isChange = False
        self.updateSetting()

    def updateSetting(self):
        self.authorTxt.setText(getToolCfg(EToolCfg.author.name))
        index = getToolCfg(EToolCfg.fieldPro.name)
        btns = self.varProGroup.buttons()
        index = min(index,len(btns)-1)
        self.proBtn = protectedBtn = btns[index]
        self.proBtn.setChecked(True)

        flag = bool(getToolCfg(EToolCfg.autoBtn.name))
        self.autoBtnCB.setChecked(flag)

        flag = bool(getToolCfg(EToolCfg.autoList.name))
        self.autoListCb.setChecked(flag)
        
        flag = bool(getToolCfg(EToolCfg.autoMkProxy.name))
        self.mkProxyCb.setChecked(flag)

        flag = bool(getToolCfg(EToolCfg.autoMkCtrl.name))
        self.mkCtrlCb.setChecked(flag)

        flag = bool(getToolCfg(EToolCfg.autoMkCache.name))
        self.mkCacheCb.setChecked(flag)

        flag = bool(getToolCfg(EToolCfg.fieldStyle.name))
        self.fieldStyleCb.setChecked(flag)
        
    def onCbClick(self,e):
        self.isChange = True

    def onAuthorChange(self):
        self.isChange = True
    
    def onClickSave(self):        
        nameStr = self.authorTxt.text()
        nameStr = str(nameStr).strip()
        setToolCfg(EToolCfg.author.name,nameStr,False)
        idx = self.varProGroup.buttons().index(self.proBtn)
        setToolCfg(EToolCfg.fieldPro.name,idx,False)
        val = 1 if self.autoBtnCB.isChecked() else 0
        setToolCfg(EToolCfg.autoBtn.name,val,False)

        val = 1 if self.autoCtrlCb.isChecked() else 0
        setToolCfg(EToolCfg.autoCtrl.name,val,False)

        val = 1 if self.fieldStyleCb.isChecked() else 0
        setToolCfg(EToolCfg.fieldStyle.name,val,False)

        val = 1 if self.mkProxyCb.isChecked() else 0
        setToolCfg(EToolCfg.autoMkProxy.name,val,False)

        val = 1 if self.mkCtrlCb.isChecked() else 0
        setToolCfg(EToolCfg.autoMkCtrl.name,val,False)

        val = 1 if self.mkCacheCb.isChecked() else 0
        setToolCfg(EToolCfg.autoMkCache.name,val,False)

        val = 1 if self.autoListCb.isChecked() else 0
        setToolCfg(EToolCfg.autoList.name,val,True)
        self.close()

    def onRadioBtnClick(self,btn):
        self.proBtn = btn
        self.isChange = True

    def getRadioBtn(self,text,px,py)->QRadioButton:
        privateBtn = QRadioButton(self)        
        privateBtn.setText(text)
        privateBtn.adjustSize()
        privateBtn.move(px,py)
        return privateBtn

    def getCheckBox(self,text,px,py,tips:str="")->QCheckBox:
        cb = QCheckBox(self)        
        cb.setText(text)
        cb.adjustSize()
        cb.move(px,py)
        cb.setToolTip(tips)
        return cb

    def closeEvent(self,e):        
        super().closeEvent(e)
        self.closed[bool].emit(self.isChange)
    def showEvent(self,e):
        super().showEvent(e)        
        self.isChange = False
        
class MyMainWin(BaseTabWin):
    def __init__(self, sizeW=800, sizeH=500, icoUrl='assets/ico/logo.png'):
        super().__init__([TabXMLToTS], "XML->TS", "工具", sizeW, sizeH, icoUrl)      
        #定义菜单动作
        settingAction = QAction(QIcon('assets/ico/logo.png'),'&设置',self)
        settingAction.setShortcut('ctrl+r')
        settingAction.setStatusTip('打开软件设置')
        settingAction.triggered.connect(self.openSetWin)
        
        descAction = QAction(QIcon('assets/ico/logo.png'),'&说明',self)
        #aboutAction.setShortcut('ctrl+r')
        descAction.setStatusTip('软件说明')
        descAction.triggered.connect(self.openDescWin)

        aboutAction = QAction(QIcon('assets/ico/logo.png'),'&关于',self)
        #aboutAction.setShortcut('ctrl+r')
        aboutAction.setStatusTip('软件版本信息')
        aboutAction.triggered.connect(self.openAboutWin)        

        importAction = QAction(QIcon('assets/ico/import.png'),'&打开',self)
        importAction.setShortcut('ctrl+o')
        importAction.setStatusTip('打开文件')
        #importAction.triggered.connect(self.openFile)

        menuBar = self.menuBar()
        settingMenu = menuBar.addMenu("文件")
        settingMenu.addAction(settingAction)
        settingMenu.addAction(descAction)
        settingMenu.addAction(aboutAction)
        
        self.setWin = None
        self.aboutWin = None

        self.aboutArr = [
            "设置好要生成代码的相关信息",
            "直接把GUI组件的xml文件拖进来即可生成TS",
            "生成已存在的TS文件手写代码会被覆盖(慎重)",
            "只会生成自定义名称的元件",
            "按钮只会生成btn_命名开头的元件,过滤掉btn_close",
            "文本只会生成txt_命名开头的元件",
            "BaseItem只会生成命名以baseitem开头的",
            "除了BaseItem不会生成组件里嵌入的组件(组件可以单独再生成一个类)",
            "不勾选忽略模块名的话，如果模块不存在会自动创建",
            "设置好模块根目录后，输入模块前几个字母会自动搜索",
            "每次打开会自动扫描assets/template基类目录，可以自己增减基类",
            "可以自己修改基类模板添加必要的信息"
        ]
        aboutLen = len(self.aboutArr)
        self.descStr = ""
        for i in range(aboutLen):
            self.descStr+=str(i+1)+"."+self.aboutArr[i]+"<br>"
        
        self.aboutStr = getVersionInfStr()

    def openSetWin(self):        
        if not self.setWin:
            self.setWin = SettingWin(self)
            self.setWin.closed.connect(self.onSetWinClose)  
        self.setWin.setVisible(True)
    def openDescWin(self):        
        QMessageBox.information(self,"使用说明",self.descStr)

    def openAboutWin(self):
        QMessageBox.information(self,"版本信息",self.aboutStr)

    def initUI(self):
        super().initUI()
    def onSetWinClose(self,flag):        
        if flag:
            self.getPanel(0).updateSetting() 

    # 每个QObject及其子类都有一个timerEvent()事件处理器。我们要重新实现这个事件处理器来响应定时器事件。
    def timerEvent(self, e):
        if self.step >= 100:
            self.doAction()
        else:
            self.step = self.step + 1
            self.proBar.setValue(self.step)


if __name__ == "__main__":
    initVersionInf(VERSION_PATH)
    initDataTypeCfg()
    initToolCfg()    
    app = QApplication(sys.argv)    
    w = MyMainWin()    
    w.show()
    sys.exit(app.exec_())
